<?php
/*
	Нижний уровень абстракции от конкретной СУБД

	DBAL_mysql( &$dbal )

	---------

	Этот класс -- low-level mysql dbal.
	Он конструируется автоматически из higher level

	---------

	// Защита строкового значения кавычками

	* Quote( $value ) -- обквочивает значение, делая его безопасным для SQL
		- возвращает что-то вроде '13' или 'строка'
			(вместе с правильными кавычками, которые вокруг добавлять уже не надо)

	// Упрощение вызовов SQL с возвратом в виде хэшей

	* Query( $sql, $limit=0, $offset=0 ) -- делает запрос, возвращая "рукоятку"
	* FetchAssoc ( $db_handle ) -- возвращает хэш фетченной строки, сдвигая "рукоятку" "вниз"
	* FetchObject( $db_handle ) -- то же, только возвращает объект
	* FreeResult( $db_handle ) -- освобождает память драйвера, сбрасывая "рукоятку"

	// Прочее

	* InsertId() -- возвращает идентификатор только что вставленной строки

	* Close()   -- закрывает всё, что может (но не все незакрытые "рукоятки")
	* Connect() -- устанавливает соединение с БД

	---------

	Этому DBAL нужны такие параметры в $rh:

	$rh->db_host     = "localhost"
	$rh->db_user     = "username"
	$rh->db_password = "pwd"
	$rh->db_name     = "project_db"

=============================================================== v.1 (kuso@npj)
 */

class RL_DBAL_mysql
{

	/**
	 * ВЫсокоуровенвый DBAL
	 *
	 * @var RL_DBAL
	 */
	private $dbal;   // higher level

	/**
	 * рукоятка к mysql db
	 *
	 * @var unknown_type
	 */
	public $dblink;

	private $log;

	public function __construct( RL_DBAL $dbal, $log=true )
	/**
	 * Конструктор
	 *
	 * @param RL_DBAL $dbal
	 */
	{
		$this->dbal = $dbal;
		$this->log = $log;
	}

	public function connect()
	/**
	 * Установить соединение с БД
	 *
	 * @return void
	 */
	{
		if(!extension_loaded("mysql")) {
			dl("mysql.so");
		}

		$this->dblink = mysql_connect($this->dbal->db_host,
			$this->dbal->db_user,
			$this->dbal->db_password
		);
		mysql_select_db($this->dbal->db_name, $this->dblink);

		if(isset($this->dbal->db_encoding) && $this->dbal->db_encoding) {
			$encoding = $this->dbal->db_encoding;
			$this->execute("SET NAMES " . $this->dbal->db_encoding);
		}

		if ($this->log) debug( "db thread_id: ". mysql_thread_id($this->dblink) );

	}
	public function close()
	/**
	 * Закрыть соединение
	 *
	 * @return void
	 */
	{
		/* в нашем случае ничего */
	}

	static $transaction_level = 0;
	public function begin()
	/**
	 * Старт транзакции
	 *
	 * @return bool
	 */
	{
		/* команда устанавливающая транзакции в режим REPEATABLE READ:
			чтение происходит на снимок таблиц от начала транзакции
			инсерт, апдейт, делет возможен, но селект по этим записям возможен только в рамках текущей транзакции
		 */
		if (self::$transaction_level <= 0) {
			$this->execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
			$this->execute("SET AUTOCOMMIT = 0");
			$this->execute("START TRANSACTION");
			$this->execute("SET @rollback = 0");
			static $q;
			if (!isset($q)) {
				$q = true;
				if ($this->log) debug(__METHOD__.'() Register shutdown function');
				register_shutdown_function(array(&$this, 'onShutdown'), 0);
			}
			self::$transaction_level = 1;
			if ($this->log) debug(__METHOD__.'() START TRANSACTION');
		} else {
			self::$transaction_level++;
			if ($this->log) debug(__METHOD__.'() start transaction '.self::$transaction_level);
		}
		return true;
	}
	public function onShutdown ()
	/**
	 * Функция, которая вызывается если скрипт умрет.
	 *
	 * НЕ ДЕЛАТЬ ЕЕ private!!!!!! :)
	 *
	 * Нужно вызывать лишь раз. ;)
	 *
	 * @access public
	 *
	 * @return void
	 */
	{
		if ($this->log) debug(__METHOD__.'()');

		$status = NULL;
		if (self::$transaction_level >= 1) { // если есть открытые транзакции, без относительно уровня
			if ($this->log) debug(__METHOD__.'() Close transactions. To see logs, drink coffee..');
			$this->execute("ROLLBACK");
			//$this->execute("SET @rollback = 0");
			if ($this->log) error(__METHOD__.'() ROLLBACK');
			$status = false;
		}
		self::$transaction_level = 0;
		return $status;
	} // end of function onShutdown
	public function rollback ()
	{
		$status = false;
		$this->execute("SET @rollback = 1"); // могут откатить специально,
		                                     // не из-за ошибки в запросе
		if (self::$transaction_level == 1 && $this->_rollback()) {
			if ($this->log) debug(__METHOD__.'() ROLLBACK');
			$status = true;
		} else {
			if ($this->log) debug(__METHOD__.'() rollback '.self::$transaction_level);
		}
		self::$transaction_level--;
		return $status;
	} // end of function rollback
	public function end ()
	{
		return $this->commit();
	} // end of function end

	public function is_rollback() {
		$rollback = $this->fetchAssoc($this->fetch("SELECT @rollback"));

		if ($rollback['@rollback'] == 1) {
			return true;
		} else return false;
	}

	public function commit ()
	{
		$status = false;
		if (self::$transaction_level == 1) {
			//$rollback = $this->fetchAssoc($this->fetch("SELECT @rollback"));
			if (!$this->is_rollback() && $this->_commit()) {
				$status = true;
			} else {
				$this->rollback();
			}

		} else {
			if ($this->log) debug(__METHOD__.'() commit '.self::$transaction_level);
		}
		self::$transaction_level--;
		return $status;
	} // end of function end

	public function _rollback ()
	{
		if ($this->log) debug(__METHOD__.'() ROLLBACK');
		$status = $this->execute('ROLLBACK');
		$this->execute("SET AUTOCOMMIT = 1");
		return $status;
	} // end of function _rollback
	public function _commit ()
	{
		if ($this->log) debug(__METHOD__.'() COMMIT');
		$status = $this->execute('COMMIT');
		$this->execute("SET AUTOCOMMIT = 1");
		return $status;
	} // end of function _rollback

	public function fetch($sql)
	/**
	 * Прямой вызов SQL
	 *
	 * @param string $sql
	 * @param int $limit
	 * @param int $offset
	 * @return resource
	 */
	{
		$this->dbal->error = NULL;

		$result = $this->make_query($sql);

		if (!$result) {
			return $this->handleError();
		}

		if (!is_resource($result)) {
			$result = FALSE;
		}

		return $result;
	}

	public function execute($sql)
	/**
	 * Выполить процедуру.
	 *
	 * Запрос который не возвращает данные.
	 *
	 * @param mixed $sql запрос
	 * @access public
	 * @return boolean
	 */
	{
		$this->dbal->error = NULL;

		$result = $this->make_query($sql);

		if (!$result) {
			return $this->handleError();
		}
		return True;
	}

	private function make_query($sql) {
		$start_time = microtime();
		$result = mysql_query($sql, $this->dblink);
		$end_time = microtime();

		if ($this->log)  {
			debug("[db_query] ".$sql);
			debug("[time] ".number_format($end_time-$start_time, 4));
		}

		return $result;
	}

	private function handleError() {
		$error = mysql_error($this->dblink);

		/* есть подозрение что не всегда нужно делать откат транзакции,
		а например делать если в транзакции была запись */
		$this->execute("SET @rollback=1");
		return $this->dbal->_Error( "Query failed: ".$sql." (".$error.")" );
	}

	public function insertId()
	/**
	 * Получить ID последней вставленной записи
	 *
	 * @return int
	 */
	{
		return mysql_insert_id( $this->dblink );
	}

	public function affectedRows( )
	/**
	 * Получить кол-во задействованных в запросе строк
	 *
	 * @return int
	 */
	{
		return mysql_affected_rows($this->dblink);
	}

	public function getNumRows($handle)
	/**
	 * Получить кол-во строк в результате выборки
	 *
	 * @param resource $handle
	 * @return int
	 */
	{
		return mysql_num_rows($handle);
	}


	//FIXME : 2007-09-28 : lucky
	//move to qlc
	public function quote ($value)
	/**
	 * Защита строкового значения кавычками
	 *
	 * @param string $value
	 * @return string
	 */
	{
		if (is_array($value)) {
			$ret = array_map(array($this, 'quote'), $value);
			$res = implode (',', $ret);
		} elseif (is_numeric($value)) {
			$res = $value;
		} else {
			$res = "'".$this->escape($value)."'";
		}
		return $res;
	} // end of function quote
	public function quoteTable ($name)
	{
		return '`'.$this->dbal->db_prefix.$this->escape($name).'`';
	} // end of function table
	public function quoteField ($name)
	{
		return ($name === '*' ? $name : '`'.$this->escape($name).'`');
	} // end of function table
	public function quoteDatabase ($name)
	{
		return '`'.$this->escape($name).'`';
	} // end of function table
	public function escape ($str)
	{
		return mysql_real_escape_string($str, $this->dblink);
	} // end of function escape

	public function fetchAssoc( $handle )
	/**
	 * Получить текущую строку в хэш
	 *
	 * @param resource $handle
	 * @return array
	 */
	{
		return mysql_fetch_assoc($handle);
	}
	public function fetchObject( $handle )
	/**
	 * Получить текущую строку в объект
	 *
	 * @param resource $handle
	 * @return stdClass
	 */
	{
		return mysql_fetch_object($handle);
	}
	public function freeResult( $handle )
	/**
	 * Освободить результат
	 *
	 * @param resource $handle
	 * @return void
	 */
	{
		return mysql_free_result($handle);
	}

	// EOC{ DBAL_mysql }
}

?>
